import axios, { AxiosError, AxiosRequestConfig } from "axios";
import { stringify } from "qs";

import { opn } from "../task/login";
import Constants from "../constant";
import { readFile } from "fs/promises";
import {join as joinPath} from "path";

const deviceCodeBaseURL =
    "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";

/**
 *
 * @param time milliseconds.
 * @returns
 */
function sleep(time: number): Promise<void> {
    return new Promise<void>((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, time);
    });
}

class MSAccount {
    refreshToken: string;
    accessToken: string;
    expire: number;

    constructor(accessToken: string, refreshToken: string, expiresIn: number) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.expire = Date.now() + expiresIn * 1000;
    }

    get expired(): boolean {
        return Date.now() >= this.expire;
    }
}

class Login {
    clientId: string;

    msLoginUserCode: string | undefined;
    msLoginDeviceCode: string | undefined;
    msLoginMessage: string | undefined;

    msLoginPollParams: string | undefined;
    msLoginPollInterval: number | undefined;

    msAccount: MSAccount | undefined;

    xblToken: string | undefined;
    xblUserHash: string | undefined;

    xstsToken: string | undefined;

    minecraftAccessToken: string | undefined;
    minecraftExpire: number | undefined;

    constructor(clientId: string) {
        this.clientId = clientId;
    }

    async getDeviceCodeAndRedirectURI(timeout: number | void): Promise<void> {
        const deviceCodeURLObject = new URL(deviceCodeBaseURL);
        deviceCodeURLObject.searchParams.set("client_id", this.clientId);
        deviceCodeURLObject.searchParams.set(
            "scope",
            "XboxLive.signin offline_access",
        );

        const deviceCodeURL = deviceCodeURLObject
            .toString()
            .replaceAll(/\+/g, "%20");
        const result = await axios.get(deviceCodeURL, {
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
            },
            timeout: timeout || 20 * 1000,
            responseType: "json",
        });

        this.msLoginDeviceCode = result.data.device_code;
        this.msLoginMessage = result.data.message;
        this.msLoginPollInterval = result.data.interval;
        this.msLoginUserCode = result.data.user_code;
    }

    async createMSLoginWindow() {
        //this.msLoginPollParams = `grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id=${this.clientId}&device_code=${this.msLoginDeviceCode}`;

        const child = await opn("https://www.microsoft.com/link");

        setTimeout(() => {
            child.kill();
        }, 200);
    }

    async refreshMSToken(
        account: MSAccount,
        timeout: number | void,
    ): Promise<void> {
        if (!account.expired) {
            this.msAccount = account;
            return;
        }

        timeout = timeout || 10 * 1000;

        try {
            const result = await axios({
                method: "post",
                url: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                data: stringify({
                    client_id: this.clientId,
                    grant_type: "refresh_token",
                    refresh_token: account.refreshToken,
                    redirect_uri: "https://login.live.com/oauth20_desktop.srf",
                }),
                timeout: timeout,
                responseType: "json",
            });

            const { access_token, refresh_token, expires_in } = result.data;

            this.msAccount = new MSAccount(
                access_token,
                refresh_token,
                expires_in,
            );
        } catch (e) {
            if (e instanceof AxiosError) {
                if (Constants.debug) {
                    console.log("error:", e.response?.data);
                }
                throw Error(
                    "refresh failed. you need to log in the account again.",
                );
            }
        }
    }

    async pollMSTokenResult(
        singleRequestTimeout: number | void | undefined | null,
        totalTimeout: number | void | undefined | null,
        account: MSAccount | void | undefined | null,
    ): Promise<void> {
        if (account) return await this.refreshMSToken(account);

        if (!this.msLoginPollInterval)
            throw Error("call after msLoginPollInterval set!");

        singleRequestTimeout = singleRequestTimeout || 10 * 1000;
        totalTimeout = totalTimeout || 5 * 60 * 1000; // 我给你五分钟的时间...

        let connectionFailure = 0;
        let start = Date.now();

        let errorMessage = "";

        while (connectionFailure < 3) {
            await sleep(this.msLoginPollInterval * 1000);

            function fail(message: string) {
                connectionFailure = 114514;
                errorMessage = message;
            }
            try {
                if (Date.now() - start > totalTimeout) {
                    connectionFailure = 114514;
                    fail("total timeout");
                    continue;
                }

                const body = stringify({
                    grant_type: "urn:ietf:params:oauth:grant-type:device_code",
                    device_code: this.msLoginDeviceCode,
                    client_id: this.clientId,
                });

                const config: AxiosRequestConfig<string> = {
                    method: "post",
                    url: "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
                    headers: {
                        "Content-Type": "application/x-www-form-urlencoded",
                        Cookie: "x-ms-gateway-slice=estsfd; stsservicecookie=estsfd; fpc=Arye8e0PjfhBpliU-Xbf39GRdhsmAgAAAELVtt0OAAAA",
                    },
                    data: body,
                    responseType: "json",
                    timeout: singleRequestTimeout,
                };

                const result = await axios(config);

                this.msAccount = new MSAccount(
                    result.data.access_token,
                    result.data.refresh_token,
                    result.data.expires_in,
                );
                return;
            } catch (e) {
                if (
                    e instanceof AxiosError &&
                    e.code == "ERR_BAD_REQUEST" &&
                    e.response
                ) {
                    const pollState = e.response.data.error;
                    switch (pollState) {
                        case "authorization_declined":
                            fail("user decline");
                            break;
                        case "bad_verification_code":
                            fail("bad device code");
                            break;
                        case "expired_token":
                            fail("expire");
                            break;
                        case "authorization_pending":
                            connectionFailure = 0;
                            if (Constants.debug) {
                                console.log("poll pending...");
                            }
                        default:
                    }

                    continue;
                }

                connectionFailure++;

                if (Constants.debug) {
                    if (e instanceof AxiosError) {
                        console.log(
                            `poll fail: ${e.code} ${e.message} : ${e.response}`,
                        );
                    } else {
                        console.log(`poll fail: ${e}`);
                    }
                }
            }
        }

        throw Error(errorMessage || "failed 3 times in a row, give up.");
    }

    async authXboxLive(
        timeout: number | null | undefined | void,
        retry: number | null | undefined | void,
    ): Promise<void> {
        if (!this.msAccount) throw Error("you need a microsoft account first!");

        timeout = timeout || 5 * 1000;
        retry = retry || 3;

        while (retry > 0) {
            try {
                let result = await axios({
                    url: "https://user.auth.xboxlive.com/user/authenticate",
                    method: "post",
                    headers: {
                        "Content-Type": "application/json",
                        Accept: "application/json",
                    },
                    data: JSON.stringify({
                        Properties: {
                            AuthMethod: "RPS",
                            SiteName: "user.auth.xboxlive.com",
                            RpsTicket: `d=${this.msAccount.accessToken}`,
                        },
                        RelyingParty: "http://auth.xboxlive.com",
                        TokenType: "JWT",
                    }),
                    timeout: timeout,
                    responseType: "json",
                });

                this.xblToken = result.data["Token"];
                this.xblUserHash = result.data["DisplayClaims"]["xui"][0]["uhs"];

                return;
            } catch (e) {
                if (e instanceof AxiosError) {
                    retry--;
                    if (Constants.debug) {
                        console.log(`axios error when getting xbl token: ${e.code} ${e.message} ${e.response?.data}`);
                    }
                } else {
                    retry--;
                    if (Constants.debug) {
                        console.log("error:", e);
                    }
                }
            }
        }

        throw Error("Xbox Live auth failed.");
    }

    async getXSTSToken(
        timeout: number | null | undefined | void,
        retry: number | null | undefined | void,
    ): Promise<void> {
        if (!this.xblToken) throw Error("you need a xbl token first!");

        timeout = timeout || 5 * 1000;
        retry = retry || 3;

        while (retry > 0) {
            try {
                let result = await axios({
                    url: "https://xsts.auth.xboxlive.com/xsts/authorize",
                    method: "post",
                    headers: {
                        "Content-Type": "application/json",
                        Accept: "application/json",
                    },
                    data: JSON.stringify({
                        Properties: {
                            SandboxId: "RETAIL",
                            UserTokens: [
                                this.xblToken
                            ]
                        },
                        RelyingParty: "rp://api.minecraftservices.com/",
                        TokenType: "JWT",
                    }),
                    timeout: timeout,
                    responseType: "json",
                });

                this.xstsToken = result.data["Token"];

                return;
            } catch (e) {
                if (e instanceof AxiosError) {
                    retry--;
                    if (Constants.debug) {
                        console.log(`axios error when getting xsts token: ${e.code} ${e.message} ${e.response?.data}`);
                    }
                } else {
                    retry--;
                    if (Constants.debug) {
                        console.log("error:", e);
                    }
                }
            }
        }

        throw Error("getting xsts token failed.");
    }

    async loginMinecraft(
        timeout: number | null | undefined | void,
        retry: number | null | undefined | void,
    ): Promise<void> {
        if (!this.xstsToken) throw Error("you need a xsts token first!");

        timeout = timeout || 5 * 1000;
        retry = retry || 3;

        while (retry > 0) {
            try {
                let result = await axios({
                    url: "https://api.minecraftservices.com/authentication/login_with_xbox",
                    method: "post",
                    headers: {
                        "Content-Type": "application/json",
                        Accept: "application/json",
                    },
                    data: JSON.stringify({
                        identityToken: `XBL3.0 x=${this.xblUserHash};${this.xstsToken}`
                    }),
                    timeout: timeout,
                    responseType: "json",
                });

                this.minecraftAccessToken = result.data["access_token"];
                this.minecraftExpire = Date.now() + result.data["expire_in"] * 1000;

                return;
            } catch (e) {
                if (e instanceof AxiosError) {
                    retry--;
                    if (Constants.debug) {
                        console.log(`axios error when logging in minecraft: ${e.code} ${e.message} ${e.response?.data}`);
                    }
                } else {
                    retry--;
                    if (Constants.debug) {
                        console.log("error:", e);
                    }
                }
            }
        }

        throw Error("Logging in minecraft failed.");
    }

    async hasMinecraftOwnership():Promise<boolean> {
        const result = await axios({
            "url": "https://api.minecraftservices.com/entitlements/mcstore",
            "method":"get",
            "headers": {
                "Authorization": `Bearer ${this.minecraftAccessToken}`,
                "Accept": "application/json"
            },
            responseType: "json",
            timeout: 10 * 1000
        });

        const items = result.data.items;

        if (!items || Array.isArray(items)) throw Error("bad response when checking ownership: no items");
        
        if (items.length < 2) return false;

        for (let i = 0; i < items.length; i++) {
            const item = items[i];

            if (item.name == "product_minecraft") return true;
            if (item.name == "game_minecraft") return true;
        }

        return true;
    }
}

async function damnTest(clientToken:string) {
    if (!clientToken) {
        try {
            clientToken = (await readFile(joinPath(__dirname, "../../test", "secrets", "client_id.txt"))).toString();
        }
        catch (e) {
            console.log(e);
            console.log("Logging In requires a client id. You should send this parameter either by including it as an argument in the command or by creating a ./test/secrets/client_id.txt file with the secret in it.");
            return;
        }
    }

    const login = new Login(clientToken);

    await login.getDeviceCodeAndRedirectURI();
    console.log("User code:", login.msLoginUserCode);

    await login.createMSLoginWindow();
    await login.pollMSTokenResult();
    await login.authXboxLive();
    await login.getXSTSToken();

    await login.loginMinecraft();

    console.log(await login.hasMinecraftOwnership());

    // refresh
    /*
    if (account) account.expire = 0;

    await login.pollMSTokenResult(null, null, account);
    const refreshedAccount = login.msAccount;

    console.log(refreshedAccount);
    */
}

export { damnTest, Login };
